Skip to content

feat: CDDL-to-Zod generator, verified against wire-mesh's conformance vectors - #3

Merged
Mearman merged 5 commits into
mainfrom
feat/zod-generator
Sep 10, 2026
Merged

feat: CDDL-to-Zod generator, verified against wire-mesh's conformance vectors#3
Mearman merged 5 commits into
mainfrom
feat/zod-generator

Conversation

@Mearman

@Mearman Mearman commented Sep 10, 2026

Copy link
Copy Markdown
Member

Builds the actual generator the foundation/spike decisions (#1, #2) committed to: emitModule walks a parsed CDDL AST and emits one Zod schema plus one inferred type per rule, scoped to the subset wire-mesh's spec/protocol.cddl uses -- maps (including the open-tail and literal-keyed-arrow-entry patterns COSE headers need), fixed/homogeneous arrays, socket unions, and the .size/.regexp/.cbor/.cborseq operators. Anything outside that subset throws a clear, rule-named error.

The vendored parser (vendor/cddl/) picks up a third upstream fix beyond the two already tracked: ? was parsing with the same unbounded upper bound as *, which made an arrow-syntax entry keyed by a literal-valued rule reference (? cose-header-alg => int) indistinguishable from the generic open-map-tail pattern in the same map. Filed as webdriverio/cddl#90, fixed on the same fork stack at Mearman/cddl#2.

test/round-trip.test.ts is the real proof: for every vector in wire-mesh's frames/handshake/tokens conformance files (copied in as fixtures alongside its real spec/protocol.cddl), it decodes the recorded wire_hex, validates the result against the generated schema, and re-encodes it -- asserting the output matches wire_hex exactly. 31/31 vectors round-trip byte-for-byte.

Two bugs the round-trip test itself caught before they'd have hit a real consumer: decoding into a Node Buffer rather than a plain Uint8Array (cbor2's subarray() slices inherit Buffer, which its own encoder doesn't recognise -- the same gotcha conformance/codec.ts documents in the reverse direction), and cborDecodesAs originally transforming a .cbor field into its decoded value rather than validating and passing the original bytes through, which broke re-encoding.

wire-mesh's CDDL spec needs three parser fixes not yet released upstream: operators on unnamed array members no longer crash the parser, .cbor/.cborseq are recognised as valid control operators, and the ? occurrence indicator no longer parses with the same unbounded upper bound as *. Each is tracked in webdriverio/cddl#87/#88/#89/#90; this vendors the patched source (compiled via its own tsc step, not modified to fit any other execution model) until they ship.
Mirrors wire-mesh's conformance/ package convention: pnpm + Node's native TS execution for scripts, tsdown for the dual ESM/CJS library build, attw verifying the published package surface, a split tsconfig (tightly scoped to the build entry vs. a looser one covering scripts/tests/configs), and underscore-prefixed turbo task names so the public package.json scripts don't recursively resolve to themselves.
emitModule walks the vendored parser's AST and produces a TypeScript module with one z.lazy()-wrapped schema plus one inferred type per rule, scoped to the CDDL subset wire-mesh's spec actually uses: maps (with at most one open key => value tail, resolving an arrow-syntax key to its literal wire value when it names a single-literal rule rather than the rule's own name), fixed and homogeneous arrays, socket unions (a rule's base definition plus every /= choice-addition, whatever mix of map/array/variable shapes they use), and the .size/.regexp/.cbor/.cborseq control operators. Anything outside that subset throws a clear, rule-named error rather than silently emitting something wrong.

cborDecodesAs (src/runtime.ts) validates a .cbor/.cborseq bstr field's embedded content against its referenced schema without transforming the field itself, so re-encoding a validated value reproduces the exact bytes that were decoded.

src/cli.ts is the thin script entry (node src/cli.ts <input.cddl> <output.ts>), run directly rather than through the packaged library surface.
For every vector in wire-mesh's frames/handshake/tokens conformance files (copied here as fixtures, alongside its real spec/protocol.cddl), decodes the recorded wire_hex, validates the result against the schema generated from protocol.cddl, and re-encodes it -- asserting the output matches wire_hex exactly. This is cddl.js's actual definition of done: not that the generator runs without throwing, but that its output round-trips real, previously-agreed wire bytes byte-for-byte.

The generated module is written to test/fixtures/generated/protocol.ts at test time (gitignored) rather than committed, since it's a deterministic function of the fixture .cddl; a sibling runtime.ts re-exports src/runtime.ts so the generated module's own "./runtime.js" import resolves without duplicating that file's content.
The generator's own definition of done -- conformance vectors round-trip byte-exactly through the generated schemas -- needs to actually run somewhere other than a developer's machine to mean anything.
@Mearman
Mearman marked this pull request as ready for review September 10, 2026 08:47
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-10T08:54:21.555155Z 3f274eb Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman merged commit e5a99ac into main Sep 10, 2026
1 check passed
@Mearman
Mearman deleted the feat/zod-generator branch September 10, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant